package bootstrap;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import jodd.typeconverter.TypeConverterManager;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.Parser;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import server.Applications;
import common.ConfigHelper;

public class Main
{
    static final String rootPathArgs = "d";
    static final String defaultRootPath = "D:/tmpFile";
    static Runner runner;
    static ScheduledExecutorService logFileServer;
    static boolean initLogOptions = false;

    public static void main(String[] args) throws Exception
    {
        sysOutAndErrOverrideWithSwitch();
        startApplication(args);
    }

    private static void sysOutAndErrOverrideWithSwitch()
    {
        String logSwitch = ConfigHelper.getStringProperty("log.switch", "off");
        if (BooleanUtils.toBoolean(logSwitch))
        {
            sysOutAndErrOverride();
        }
    }

    private static class LogSystemTask implements Runnable
    {
        public LogSystemTask()
        {
            if (!initLogOptions)
            {
                initLogFile();
                initLogOptions = true;
            }
        }

        String logRootPath = ConfigHelper.getStringProperty("log.path", "log");
        Date stdLogCreateTime = new Date();
        Date errLogFileCreateTime = new Date();

        public void run()
        {
            reCreateLog();
        }

        private void sleep()
        {
            String outFileName = ConfigHelper.getStringProperty("log.recreate.time", "02:00");
            Calendar cal = Calendar.getInstance();
            int hour = cal.get(Calendar.HOUR_OF_DAY);
            int minute = cal.get(Calendar.MINUTE);
            int seconds = cal.get(Calendar.SECOND);
            String timeSeparator = ":";
            String[] strs = StringUtils.split(outFileName, timeSeparator);
            int count = strs.length;
            int targetHour = 2;
            int targetMin = 0;
            int targetSec = 0;
            switch (count)
            {
            case 2:
                targetHour = NumberUtils.toInt(strs[0]);
                targetMin = NumberUtils.toInt(strs[1]);
                targetSec = NumberUtils.toInt(strs[2]);
                break;
            case 1:
                targetHour = NumberUtils.toInt(strs[0]);
                targetMin = NumberUtils.toInt(strs[1]);
                break;
            case 0:
                if (NumberUtils.isNumber(outFileName))
                {
                    targetHour = NumberUtils.toInt(outFileName);
                }
                break;
            default:
                break;

            }

            int leftHour = targetHour > hour ? (targetHour - hour) : (24 - hour + targetHour);
            int leftMin = targetMin > minute ? (targetMin - minute) : (60 - hour + targetHour);
            int leftSec = targetSec > seconds ? (targetSec - seconds) : (60 - hour + targetHour);
            int sleepMillis = (leftHour * 3600 + leftMin * 60 + leftSec) * 1000 + 3;

            try
            {
                Thread.currentThread().sleep(sleepMillis);
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }

        private File getFileAndSetReadableWritable(File root, String fileName) throws IOException
        {
            File file = new File(root, fileName);
            if (!file.exists())
            {

                file.createNewFile();
                file.setWritable(true);
                file.setReadable(true);
            }
            FileUtils.writeStringToFile(file,
                String.format("----------------\n Reload Time : %s \n---------------", stdLogCreateTime), true);
            return file;
        }

        private File getRootFile()
        {
            File rootPath = new File(logRootPath);
            if (!rootPath.exists())
            {
                rootPath.mkdirs();
            }
            return rootPath;
        }

        private File stdNewLogFile(final File file) throws IOException
        {
            File newFile = needNewLogFile(file, stdLogCreateTime, "log.stdout", true);
            if (!file.equals(newFile))
            {
                stdLogCreateTime = new Date(newFile.lastModified());
            }
            return newFile;
        }

        private File errNewLogFile(final File file) throws IOException
        {
            File newFile = needNewLogFile(file, errLogFileCreateTime, "log.errout", true);
            if (!file.equals(newFile))
            {
                errLogFileCreateTime = new Date(newFile.lastModified());
            }
            return newFile;
        }

        private File needNewLogFile(final File file, Date createTime, String logProperty,
                        boolean needCreateNewFile) throws IOException
        {
            // check date
            Calendar cDay = java.util.Calendar.getInstance();
            Calendar lastRecordDay = java.util.Calendar.getInstance();
            lastRecordDay.setTime(createTime);
            int compareScope = ConfigHelper.getScopeNumberProperty(logProperty, "timeunit", Calendar.AM_PM);
            File newFile = file;
            if (cDay.get(compareScope) != lastRecordDay.get(compareScope))
            {
                // if need create a new file then update the CreatTime property.
                newFile = getDateFile(file, "D_", cDay.getTime());
            }
            // check log file size. default size is 5M.
            int fileSize = ConfigHelper.getScopeNumberProperty(logProperty, "logsize", 1024 * 1024 * 1024 * 5);
            if (file.length() > fileSize)
            {
                // if need create a new file then update the CreatTime property.
                newFile = getDateFile(file, "MS_", cDay.getTime());
            }
            if (needCreateNewFile && !file.equals(newFile))
            {
                FileUtils.copyFile(file, newFile);
            }
            return newFile;
        }

        private File getDateFile(File file, String prefix, Date creatTime) throws IOException
        {
            File source = file;
            String fileName = source.getName();
            String postfix = StringUtils.substringAfterLast(fileName, ".");
            String baseName = StringUtils.substringBeforeLast(fileName, ".");
            String dateFileName =
                            String.format("%s%s_%tm-%td-%ty_%s", prefix, baseName, creatTime,
                                creatTime,
                                creatTime, postfix);
            File logFile = new File(source.getParent(), dateFileName);
            if (!logFile.exists())
            {
                return logFile;
            }

            dateFileName =
                            String.format("%s_%s", dateFileName,
                                String.valueOf(creatTime.getTime()).substring(3, 10));

            return new File(source.getParent(), dateFileName);
        }

        private File getStdOutFile() throws IOException
        {
            return getFileAndSetReadableWritable(getRootFile(),
                ConfigHelper.getStringProperty("log.stdout.name", "out.log"));
        }

        private File getErrOutFile() throws IOException
        {
            return getFileAndSetReadableWritable(getRootFile(),
                ConfigHelper.getStringProperty("log.errout.name", "err.log"));
        }

        private void reCreateLog()
        {
            Date CurrentlyDateTime = new Date();
            try
            {
                File outFile = getStdOutFile();
                File newFile = stdNewLogFile(outFile);
                if (!newFile.equals(outFile))
                {
                    // close the output stream
                    PrintStream outStr = System.out;
                    outStr.flush();
                    outStr.close();
                    // remove the file
                    FileUtils.deleteQuietly(outFile);
                    // new the out put log file
                    outFile = getStdOutFile();
                    System.setOut(new PrintStream(new FileOutputStream(outFile)));
                }

                File errFile = getErrOutFile();
                newFile = errNewLogFile(errFile);
                if (!newFile.equals(errFile))
                {
                    PrintStream errStr = System.err;
                    errStr.flush();
                    errStr.close();
                    FileUtils.deleteQuietly(errFile);
                    errFile = getErrOutFile();
                    System.setErr(new PrintStream(new FileOutputStream(errFile)));
                }

            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
        }

        private void initLogFile()
        {
            try
            {
                File outFile = getStdOutFile();
                if (outFile.length() > 0)
                {
                    File newFile = getDateFile(outFile, "D_", stdLogCreateTime);
                    FileUtils.copyFile(outFile, newFile);
                }
                PrintStream outStr = System.out;
                outStr.flush();
                outStr.close();

                System.setOut(new PrintStream(new FileOutputStream(outFile)));

                File errFile = getErrOutFile();
                if (errFile.length() > 0)
                {
                    File newFile = getDateFile(errFile, "D_", errLogFileCreateTime);
                    FileUtils.copyFile(errFile, newFile);
                }
                PrintStream errStr = System.err;
                errStr.flush();
                errStr.close();

                System.setErr(new PrintStream(new FileOutputStream(errFile)));

            }
            catch (IOException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }

    }

    private static void sysOutAndErrOverride()
    {
        if (null == logFileServer)
        {
            logFileServer = Executors.newScheduledThreadPool(1);
            initLogOptions = false;
        }

        logFileServer.scheduleAtFixedRate(new LogSystemTask(), 1, 6, TimeUnit.HOURS);

    }

    public static void startApplication(String[] args) throws Exception
    {
        Runner runner = onStart(args);
        run(runner);
    }

    public static void run(Runner runner)
    {
        runner.run();
    }

    public static Runner onStart(String[] args) throws Exception
    {
        if (null == runner)
        {
            runner = new Runner(args)
            {
                @SuppressWarnings("unchecked")
                public void onstart(Object inputObject) throws Exception
                {
                    Map<String, Object> map = (Map<String, Object>) inputObject;
                    app = new Applications(map);
                    app.run();
                }
            };
        }
        return runner;
    }

    public static class PackageInfo
    {
        public final static String version = "0.0.1";
        public final static String projName = "BIG";
    }

    public enum OptionIndex
    {
        SINGLE_CHAR_ARG_NAME, LONG_CHAR_ARG_NAME, HAS_ARGUMENT, IS_REQUIRED, ARGUMENT_VALUE_NAME, DESCRIPTION, HELPER_TEST_CASE, CONVERT_TYPE;
    }

    static abstract class Runner implements Runnable
    {
        FileUploadCommandLineParser parser;
        Applications app;

        public Runner(String[] args) throws Exception
        {
            parser = new FileUploadCommandLineParser(args);
        }

        public abstract void onstart(Object inputObject) throws Exception;

        public void run()
        {
            if (null != app && app.isRunable())
            {
                return;
            }
            // if (parser.getValue(cli, argsName, defaultValue))
            if (!parser.validate())
            {
                parser.printHelp();
                return;
            }
            else
            {
                try
                {
                    onstart(parser.parser());
                }
                catch (Exception e)
                {
                    e.printStackTrace();
                }

            }
        }

        public void setParser(FileUploadCommandLineParser parser)
        {
            this.parser = parser;
        }

        public void shutdown() throws Exception
        {
            app.shutdown();
        }

        public void start() throws Exception
        {
            app.run();
        }
    }

    public static class FileUploadCommandLineParser
    {
        Parser parser;
        final static String PROJ_VER = String.format("%s-%s", PackageInfo.projName, PackageInfo.version);
        static ArrayList<Object[]> optionsDefs = new ArrayList<Object[]>();
        Options options;
        Runner runner;
        String[] args;
        CommandLine cli;
        ArrayList<String> needValidateArgumentList = new ArrayList<String>();
        static
        {
            // <li> opt : option single-character name;
            // <li> longOpt : Long multi-character name;
            // <li> hasArg : has argument;
            // <li> isRequired : argument need validate;
            // <li> argName : argument value display name;
            // <li> description : the command description;
            // <li> helpTestCase : help display case value ;
            // <li> convertType : the argument value convert specific class,Default is String;
            // optionsDefs.add(new Object[] { "d", "dir", true, true, "direcotry",
            // "Image root directory", "E:/tmpFile" });
            optionsDefs.add(new Object[] { "p", "pub", true, false, "publicPort", "Public Port", "41801" });
            optionsDefs.add(new Object[] { "r", "pri", true, false, "privatePort", "Private Port", "41802" });
            optionsDefs.add(new Object[] { "k", "keep", true, false, "keepAlive", "keep Alive", "0" });
            optionsDefs.add(new Object[] { "q", "que", true, false, "acceptQueueSize", "Accept Queue Size", "5" });
            optionsDefs.add(new Object[] { "b", "buf", true, false, "sendBufferSize", "send Buffer Size", "1000000" });
            optionsDefs.add(new Object[] { "x", "max", true, false, "maxThreads", "max Threads", "1000" });
            optionsDefs.add(new Object[] { "m", "min", true, false, "minThreads", "min Threads", "1000" });

        }

        public FileUploadCommandLineParser(String[] args) throws Exception
        {
            this.args = args;
            init();
        }

        private void init() throws ParseException
        {
            parser = new GnuParser();
            options = new Options();
            needValidateArgumentList.clear();

            for (Object[] args : optionsDefs)
            {
                String opt = getOptionParameter(args, OptionIndex.SINGLE_CHAR_ARG_NAME);
                String longOpt = getOptionParameter(args, OptionIndex.LONG_CHAR_ARG_NAME);
                Boolean hasArg = getOptionParameter(args, OptionIndex.HAS_ARGUMENT);
                Boolean isRequired = getOptionParameter(args, OptionIndex.IS_REQUIRED);
                String description = getOptionParameter(args, OptionIndex.DESCRIPTION);
                String argName = getOptionParameter(args, OptionIndex.ARGUMENT_VALUE_NAME);

                Option option = new Option(opt, longOpt, hasArg, description);
                option.setArgName(argName);
                option.setRequired(isRequired);
                options.addOption(option);

                if (isRequired)
                {
                    needValidateArgumentList.add(opt);
                }

            }
            cli = parser.parse(options, args);
        }

        public boolean validate()
        {
            for (String opt : needValidateArgumentList)
            {
                if (!cli.hasOption(opt))
                {
                    return false;
                }
            }
            return true;
        }

        public void printHelp()
        {
            printHelp(this.options);
        }

        public Map<String, Object> parser() throws ParseException
        {

            LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>();

            for (Object[] opts : optionsDefs)
            {
                String opt = getOptionParameter(opts, OptionIndex.SINGLE_CHAR_ARG_NAME);
                String longOpt = getOptionParameter(opts, OptionIndex.LONG_CHAR_ARG_NAME);
                Class convertClzz = getOptionParameter(opts, OptionIndex.CONVERT_TYPE);

                if (cli.hasOption(opt) || cli.hasOption(longOpt))
                {
                    String strValue = cli.getOptionValue(opt);

                    String mappingArgument = getOptionParameter(opts, OptionIndex.ARGUMENT_VALUE_NAME);
                    if (null == mappingArgument)
                    {
                        mappingArgument = opt != null ? opt : longOpt;
                    }
                    if (!String.class.equals(convertClzz))
                    {
                        map.put(mappingArgument, TypeConverterManager.convertType(strValue, convertClzz));
                    }
                    else
                    {
                        map.put(mappingArgument, strValue);
                    }
                }

            }

            return map;
        }

        private void printHelp(Options options)
        {
            String command = String.format("java -jar %s.jar [options]", PROJ_VER);
            HelpFormatter f = new HelpFormatter();
            String header = "where options include:";
            StringBuffer footer = new StringBuffer("example: \n");
            footer.append(String.format("java -jar %s.jar %s", PROJ_VER, getArgsTestCase()));
            footer.append("\n");

            f.printHelp(command, header, options, footer.toString());
        }

        private Object getArgsTestCase()
        {
            StringBuffer sb = new StringBuffer();
            for (Object[] args : optionsDefs)
            {
                String opt = getOptionParameter(args, OptionIndex.SINGLE_CHAR_ARG_NAME);
                String testCase = getOptionParameter(args, OptionIndex.HELPER_TEST_CASE);

                sb.append(String.format(" -%s %s", opt, testCase));
            }
            return sb.toString();
        }

        public Object[] getOption(int index)
        {
            if (index > optionsDefs.size())
            {
                return null;
            }
            return optionsDefs.get(index);
        }

        @SuppressWarnings("unchecked")
        public <T> T getOptionParameter(Object[] objs, OptionIndex opIndex)
        {
            if (OptionIndex.CONVERT_TYPE.equals(opIndex))
            {
                if (objs.length < OptionIndex.values().length)
                {
                    return (T) String.class;
                }
            }
            return objs == null ? null : (T) objs[opIndex.ordinal()];
        }

        @SuppressWarnings("unchecked")
        public <T> T getOptionParameter(int index, OptionIndex opIndex)
        {
            Object[] objs = getOption(index);
            return (T) getOptionParameter(objs, opIndex);
        }
    }

}
